package de.haw.vs.election;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

public class SensorNode {

  private static int SYNC_PHASE_WARTEZEIT = 20000;
  private final static int PRESENTATE_RESULTS = 5000;
  private final static int COMMITMENT_PHASE_WARTEZEIT = 5000;
  private final static int REVEALMENT_PHASE_WARTEZEIT = COMMITMENT_PHASE_WARTEZEIT;
  public final static String MESSAGE_PARTS_SEPARATOR = "##";
  public final static String SYNC_MESSAGE = "SyncMessage";
  public final static String COMMITMENT_MESSAGE = "Commitment";
  public final static String REVEALMENT_MESSAGE = "Revealment";

  private final static String MULTICAST_GROUP = "225.4.5.6";
  private final InetAddress multicastGroupAddress;

  private final int myPort;
  private final String myUUID = UUID.randomUUID().toString();

  private final MulticastSocket sendMulticast;
  private final MulticastSocket receiveMulticast;

  private final Map<String, Integer> empfangeneHashedCommitments = new TreeMap<String, Integer>();
  private final List<Commitment> empfangeneKlartextCommitments = new ArrayList<Commitment>();

  private Commitment commitment;

  public SensorNode(int p) throws IOException {
	myPort = p;
	
	System.out.println("Running on " + MULTICAST_GROUP + ", Port: " + myPort);
	
    sendMulticast = new MulticastSocket(myPort);
    receiveMulticast = new MulticastSocket(myPort);

    sendMulticast.setTimeToLive(255);
    receiveMulticast.setTimeToLive(255);

    multicastGroupAddress = InetAddress.getByName(MULTICAST_GROUP);

    sendMulticast.joinGroup(multicastGroupAddress);
    receiveMulticast.joinGroup(multicastGroupAddress);

  }

  private void sendStringMessage(String message) throws IOException {

    byte[] sendData = (myUUID + MESSAGE_PARTS_SEPARATOR + message).getBytes();
    System.out.println("SendData = " + new String(sendData));
    DatagramPacket send = new DatagramPacket(sendData, sendData.length, multicastGroupAddress, myPort);
    sendMulticast.send(send);
  }

  private String receiveStringMessage() throws IOException {
    DatagramPacket empfangeneDaten = new DatagramPacket(new byte[1024], 1024);

    receiveMulticast.receive(empfangeneDaten);
    String empfangeneNachicht = getStringFromDatagram(empfangeneDaten);
    return empfangeneNachicht;
  }

  private String getStringFromDatagram(DatagramPacket packet) {
    return new String(packet.getData(), 0, packet.getLength());
  }

  private void sendeCommitment() throws IOException {

    commitment = Commitment.newRandomCommitment(myUUID);
    System.out.println("Commitment von Knoten mit UUID: " + myUUID + ", hashCode(" + commitment.getValue() + ")=" + commitment.getHashValue());
    sendStringMessage(COMMITMENT_MESSAGE + MESSAGE_PARTS_SEPARATOR + String.valueOf(commitment.getHashValue()));
  }

  private void empfangeCommitments() throws SocketException {
    receiveMulticast.setSoTimeout(COMMITMENT_PHASE_WARTEZEIT);
    boolean time = true;
    // warten auf Antworten
    while (time) {
      try {
        // empfaengt neue Nachricht im byte-Array-Format
        String empfangeneNachicht = receiveStringMessage();
        // wenn die Nachricht == "TimeResets"
        if (isCommitmentProcessed(empfangeneNachicht)) {
          // verlaengern
          receiveMulticast.setSoTimeout(COMMITMENT_PHASE_WARTEZEIT);
          System.out.println("Neue Commitment: " + empfangeneNachicht);
        } else {
          System.out.println("Neue Commitment????: " + empfangeneNachicht);
        }

      } catch (Exception e) {
        time = false;
      }
    }
  }

  private boolean isCommitmentProcessed(String empfangeneNachicht) {

    String[] msgParts = empfangeneNachicht.split(MESSAGE_PARTS_SEPARATOR);

    if (msgParts.length == 3) {
      String sender = msgParts[0];
      String command = msgParts[1];
      String hash = msgParts[2];

      if (command.equals(COMMITMENT_MESSAGE)) {

        try {
          int hashInt = Integer.parseInt(hash);
          empfangeneHashedCommitments.put(sender, hashInt);
          return true;
        } catch (NumberFormatException e) {
          e.printStackTrace();
        }
      }
    }

    return false;
  }

  private void sendeRevealment() throws IOException {

    System.out.println("Revealment von Knoten mit UUID: " + myUUID + ", hashCode(" + commitment.getValue() + ")=" + commitment.getHashValue());
    sendStringMessage(REVEALMENT_MESSAGE + MESSAGE_PARTS_SEPARATOR + String.valueOf(commitment.getValue()));
  }

  private void empfangeRevealments() throws SocketException {
    receiveMulticast.setSoTimeout(REVEALMENT_PHASE_WARTEZEIT);
    boolean time = true;
    // warten auf Antworten
    while (time) {
      try {
        // empfaengt neue Nachricht im byte-Array-Format
        String empfangeneNachicht = receiveStringMessage();
        // wenn die Nachricht == "TimeResets"
        if (isRevealmentProcessed(empfangeneNachicht)) {
          // verlaengern
          receiveMulticast.setSoTimeout(REVEALMENT_PHASE_WARTEZEIT);
          System.out.println("Neue Revealment: " + empfangeneNachicht);
        }
      } catch (Exception e) {
        time = false;
      }
    }
  }

  private boolean isRevealmentProcessed(String empfangeneNachicht) {
    String[] msgParts = empfangeneNachicht.split(MESSAGE_PARTS_SEPARATOR);

    if (msgParts.length == 3) {

      String sender = msgParts[0];
      String command = msgParts[1];
      String klartext = msgParts[2];

      if (command.equals(REVEALMENT_MESSAGE)) {

        try {
          int klartextInt = Integer.parseInt(klartext);
          empfangeneKlartextCommitments.add(Commitment.valueOf(sender, klartextInt));
          return true;
        } catch (NumberFormatException e) {
          e.printStackTrace();
        }
      }
    }

    return false;
  }

  private void calcCoordinator() {

    final List<Commitment> validCommitments = new ArrayList<Commitment>();

    long sum = 0;

    for (Commitment com : empfangeneKlartextCommitments) {
      if (com.validateHash(empfangeneHashedCommitments.get(com.getSenderUuid()))) {
        sum += com.getValue();
        validCommitments.add(com);
      } else {
        System.err.println("Commitment " + com + " kann nich ber�cksichtigt werden");
      }
    }

    // Mapping

    if (validCommitments.isEmpty()) {
      System.out.println("Leider haben wir keinen Koordinator (alle gelogen???)");
    } else {
      Collections.sort(validCommitments);
      int coordinatorIndex = (int) (sum % validCommitments.size());

      Commitment coordinatorCommitment = validCommitments.get(coordinatorIndex);

      
      // print debug info
      for(int i = 0; i<validCommitments.size();i++) {
        System.out.print(".");
      }
      
      System.out.println();

      if (myUUID.equals(coordinatorCommitment.getSenderUuid())) {
        System.out.println("Ich bin der neue Koordiantor: " + coordinatorCommitment.getSenderUuid());
      } else {
        System.out.println("Der neue Koordiantor ist: " + coordinatorCommitment.getSenderUuid());
      }
    }

  }

  private boolean isNeueAnmeldung(String nachricht) {
    String[] nachrichtParts = nachricht.split(MESSAGE_PARTS_SEPARATOR);

    if (nachrichtParts.length == 2) {
      String anmeldung = nachrichtParts[1];

      if (anmeldung.equals(SYNC_MESSAGE)) {
        return true;
      }
    }

    return false;
  }

  private void sendSync() throws IOException {

    System.out.println("Syncversuch von Knoten mit UUID: " + myUUID);

    sendStringMessage(SYNC_MESSAGE);
  }

  private void warteSyncs() throws SocketException {
    boolean time = true;
    receiveMulticast.setSoTimeout(SYNC_PHASE_WARTEZEIT);
    // warten auf Antworten
    while (time) {
      try {
        // empfaengt neue Nachricht im byte-Array-Format
        String empfangeneNachicht = receiveStringMessage();
        // wenn die Nachricht == Sync
        if (isNeueAnmeldung(empfangeneNachicht)) {
          // verlaengern
          receiveMulticast.setSoTimeout(SYNC_PHASE_WARTEZEIT);
          System.out.println("Neue Sync: " + empfangeneNachicht);
        } else {
        	System.out.println("WarteSync: " + empfangeneNachicht);
        }
      } catch (Exception e) {
        time = false;
      }
    }
  }

  // ---------------------------------------------------------------------------------------------
  public void run() {

    try {

      sendSync();
      warteSyncs();
       while (true) {
      

      // wichtig
      Thread.sleep(1000);
      
      /* Vorbereitung */
      empfangeneHashedCommitments.clear();
      empfangeneKlartextCommitments.clear();

      System.out.println(new Date());
      System.out.println("----------------------------Commitment Phase------------------------------");
      sendeCommitment();
      empfangeCommitments();
      
      
      // wichtig
      Thread.sleep(1000);

      System.out.println(new Date());
      System.out.println("----------------------------Reveal Phase------------------------------");
      sendeRevealment();
      empfangeRevealments();

      System.out.println(new Date());
      System.out.println("----------------------------Entscheidungs Phase------------------------------");
      calcCoordinator();

      try {
        Thread.sleep(PRESENTATE_RESULTS);
      } catch (InterruptedException e) {
        // TODO: handle exception
      }
       }

    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) throws Exception {
    System.out.println(InetAddress.getLocalHost().getHostAddress());
    int p = -1;
    try {
    	p = Integer.parseInt(args[0]);
    } catch (Exception e) {
		System.out.println("Problem bei Argumenten.. bitte port als Argument uebergeben");
		throw e;
	}
    
    new SensorNode(p).run();
  }

}
